(* error.sml
 *
 * COPYRIGHT (c) 2007 The Manticore Project (http://manticore.cs.uchicago.edu)
 * All rights reserved.
 *
 * Common infrastructure for error reporting in the Manticore compiler.
 *)

structure CurrentSourcemap = struct
   val sourcemap = ref (AntlrStreamPos.mkSourcemap())
end

structure Error :> sig

  (* logical positions in the input stream *)
    type pos = AntlrStreamPos.pos
    type span = {file: AntlrStreamPos.sourcemap, span: AntlrStreamPos.span}

    type err_stream

  (* make an error stream. *)
    val mkErrStream : string -> err_stream
    val mkErrStream' : unit -> err_stream

    val anyErrors : err_stream -> bool
    val sourceFile : err_stream -> string
    val sourceMap : err_stream -> AntlrStreamPos.sourcemap

  (* add error messages to the error stream *)
    val error : err_stream * string list -> unit
    val errorAt : err_stream * span * string list -> unit

  (* add warning messages to the error stream *)
    val warning : err_stream * string list -> unit
    val warningAt : err_stream * span * string list -> unit

  (* add an ml-antlr parse error to the error stream *)
    val parseError : ('tok -> string)
	  -> err_stream
	    -> (pos * 'tok AntlrRepair.repair_action)
	      -> unit

  (* print the errors to an output stream *)
    val report : TextIO.outstream * err_stream -> unit

  (* source-code locations *)
    datatype location
      = UNKNOWN
      | LOC of {file : string, l1 : int, c1 : int, l2 : int, c2 : int}

    val location : err_stream * span -> location
    val position : err_stream * pos -> location

    (*val locToString : location -> string (*totally broken*)*)
    val spanToString : span -> string
    
  (* a term marked with a source-map span *)
    type 'a mark = {span : span, tree : 'a}

  end = struct

    structure SP = AntlrStreamPos
    structure Repair = AntlrRepair
    structure F = Format

    type pos = SP.pos
    type span = {file: SP.sourcemap, span: SP.span}

    datatype severity = WARN | ERR

    type error = {
	kind : severity,
	pos : span option,
	msg : string
      }

  (* an error stream collects the errors and warnings generated for
   * a compilation unit.
   *)
    datatype err_stream = ES of {
	srcFile		: string,
	sm		: SP.sourcemap,	(* the source map for mapping positions to *)
					(* source-file positions *)
	errors		: error list ref,
	numErrors	: int ref,
	numWarnings	: int ref
      }

  (* make an error stream. *)
    fun mkErrStream filename = ES{
	    srcFile = filename,
	    sm = SP.mkSourcemap' filename,
	    errors = ref [],
	    numErrors = ref 0,
	    numWarnings = ref 0
	  }

    fun mkErrStream' filename = ES{
	    srcFile = "<unkown>",
	    sm = SP.mkSourcemap (),
	    errors = ref [],
	    numErrors = ref 0,
	    numWarnings = ref 0
	  }

    fun anyErrors (ES{numErrors, ...}) = (!numErrors > 0)
    fun sourceFile (ES{srcFile, ...}) = srcFile
    fun sourceMap (ES{sm, ...}) = sm

    fun addErr (ES{errors, numErrors, ...}, pos, msg) = (
	  numErrors := !numErrors + 1;
	  errors := {kind=ERR, pos=pos, msg=msg} :: !errors)
	  
    fun addWarn (ES{errors, numWarnings, ...}, pos, msg) = (
	  numWarnings := !numWarnings + 1;
	  errors := {kind=WARN, pos=pos, msg=msg} :: !errors)

    fun parseError tok2str es (pos, repair) = let
	  val toksToStr = (String.concatWith " ") o (List.map tok2str)
	  val msg = (case repair
		 of Repair.Insert toks => ["syntax error; try inserting \"", toksToStr toks, "\""]
		  | Repair.Delete toks => ["syntax error; try deleting \"", toksToStr toks, "\""]
		  | Repair.Subst{old, new} => [
			"syntax error; try substituting \"", toksToStr new, "\" for \"",
			toksToStr old, "\""
		      ]
		  | Repair.FailureAt tok => ["syntax error at ", tok2str tok]
		(* end case *))
	  in
	    addErr (es, SOME{file=sourceMap es,span=(pos,pos)}, String.concat msg ^ "\n")
	  end

  (* add error messages to the error stream *)
    fun error (es, msg) = addErr (es, NONE, String.concat msg)
    fun errorAt (es, span, msg) = addErr (es, SOME span, String.concat msg)

  (* add warning messages to the error stream *)
    fun warning (es, msg) = addWarn (es, NONE, String.concat msg)
    fun warningAt (es, span, msg) = addWarn (es, SOME span, String.concat msg)

  (* sort a list of errors by position in the source file *)
    val sort = let
          fun fname sm = Option.getOpt (SP.fileName sm 0, "")
	  fun lt (NONE, NONE) = false
	    | lt (NONE, _) = true
	    | lt (_, NONE) = false
	    | lt (SOME{file=f1,span=(l1, r1)}, SOME{file=f2,span=(l2, r2)}) =
               (case String.compare (fname f1, fname f2)
                 of LESS => true
                  | GREATER => false
                  | EQUAL =>
                     (case Position.compare(l1, l2)
		       of LESS => true
		        | EQUAL => (Position.compare(r1, r2) = LESS)
		        | GREATER => false))
	  fun cmp (e1 : error, e2 : error) = not (lt(#pos e1, #pos e2))
	  in
	    ListMergeSort.sort cmp
	  end

  (* source-code locations *)
    datatype location
      = UNKNOWN
      | LOC of {file : string, l1 : int, c1 : int, l2 : int, c2 : int}

   (* FIXME *)
    fun location (ES{sm, ...}, {span=(p1, p2),...}: span) =
	  if (p1 = p2)
	    then let
	      val {fileName=f, lineNo, colNo} = SP.sourceLoc sm p1
         val f = case f of SOME f => f | NONE => ""
	      in
		LOC{file=f, l1=lineNo, c1=colNo, l2=lineNo, c2=colNo}
	      end
	    else let
	      val {fileName=f1, lineNo=l1, colNo=c1} = SP.sourceLoc sm p1
	      val {fileName=f2, lineNo=l2, colNo=c2} = SP.sourceLoc sm p2
         val f = case f1 of SOME f => f | NONE => case f2 of SOME f => f | NONE => ""
	      in
		      LOC{file=f, l1=l1, c1=c1, l2=l1, c2=c1}
	      end

    (* FIXME *)
    fun position (ES{sm, ...}, p : pos) = let
	  val {fileName=SOME f, lineNo, colNo} = SP.sourceLoc sm p
	  in
	    LOC{file=f, l1=lineNo, c1=colNo, l2=lineNo, c2=colNo}
	  end

    fun locToString UNKNOWN = "<unknown>"
      | locToString (LOC{file, l1, l2, c1, c2}) =
	  if (l1 = l2)
	    then if (c1 = c2)
	      then F.format "[%s:%d.%d] " [F.STR file, F.INT l1, F.INT c1]
	      else F.format "[%s:%d.%d-%d] " [F.STR file, F.INT l1, F.INT c1, F.INT c2]
	    else F.format "[%s:%d.%d-%d.%d] " [
		F.STR file, F.INT l1, F.INT c1, F.INT l2, F.INT c2
	      ]


      fun spanToString {file=sm,span=(p1, p2)} = if (p1 = p2)
      		    then let
      		      val {fileName=f, lineNo, colNo} = SP.sourceLoc sm p1
                   val f = case f of SOME f => f | NONE => ""
      		      in
      			F.format "%s:%d.%d" [
      			    F.STR f, F.INT lineNo, F.INT colNo
      			  ]
      		      end
      		    else let
      		      val {fileName=f1, lineNo=l1, colNo=c1} = SP.sourceLoc sm p1
      		      val {fileName=f2, lineNo=l2, colNo=c2} = SP.sourceLoc sm p2
                   val f1 = case f1 of SOME f => f | NONE => ""
                   val f2 = case f2 of SOME f => f | NONE => ""
      		      in
      			if (f1 <> f2)
      			  then F.format "%s:%d.%d-%s:%d.%d" [
      			      F.STR f1, F.INT l1, F.INT c1,
      			      F.STR f2, F.INT l2, F.INT c2
      			    ]
      			else if (l1 <> l2)
      			  then F.format "%s:%d.%d-%d.%d" [
      			      F.STR f1, F.INT l1, F.INT c1,
      			      F.INT l2, F.INT c2
      			    ]
      			  else F.format "%s:%d.%d-%d" [
      			      F.STR f1, F.INT l1, F.INT c1, F.INT c2
      			    ]
      		      end

    fun printError (outStrm, es) {kind, pos, msg} =
     let
        val kind = (case kind of ERR => "Error" | Warn => "Warning")
        val pos = case pos of NONE => "[no position] " 
                            | SOME sp => "[" ^ spanToString sp ^ "] "
     in
        TextIO.output (outStrm, String.concat [pos, kind, ": ", msg])
     end

    fun report (outStrm, es as ES{errors, numErrors, ...}) =
	  List.app (printError (outStrm, es)) (sort (!errors))

  (* a term marked with a source-map span *)
    type 'a mark = {span : span, tree : 'a}

  end
